In Search of The XPM Format

By Chris Van Haren

This series of articles on X Windows is by Chris VanHaren, who has worked with the X Window System and Motif since its early days. He spent several years at MIT's Project Athena before joining OpenVision Technologies, Inc. He can be reached at vanharen@ov.com.

Introduction and history

The X BitMap (XBM) format has been around for several years and is certainly familiar to anyone who has done any programming in X, and even to many users who have never written a line of code. XBM was designed with the following properties in mind:

For a while, the XBM format proved sufficient, and still does have its uses today. However, there are a number of shortcomings inherent in its design:

For these reasons, and others, the X PixMap (XPM) format was born. XPM has a number of unique features which makes it suitable for creating icons and small images used in X applications. Like XBM, they are stored in plain Ascii. Unlike XBM, they are much more human-readable. They are directly includable in C programs, and they can be used to define both the clipping shape or mask and the definition of the Pixmap in a single file.

Pixmaps define multi-plane images. In other words, color! XPM allows for the definition of color, greyscale and monochrome images in one file format. Additionally, symbolic names may be used for each of the colors in the file, which allows for the possibility of user-specified customization of an image without having to replace the XPM file. A library of function calls is also provided to simplify the use of the XPM format, and provides functionality similar to those available in Xlib for reading, writing, and creating Bitmaps.

Since its introduction in 1989, XPM has gained fairly widespread acceptance. It was present in the contrib directories of both R4 and R5, as well as the just-released R6. Motif 2.0 supports the use of the XPM format for creating label and button pixmaps. And it has been used for many years by several commercial products such as BuilderXcessory and TeleUSE. Indeed, it has quickly become a de-facto standard even though it has never been an official part of the X Window System.

Let's take a brief look at the XPM file format, but first...

How to get it

Before we go anywhere, first we need to get, compile, and install XPM. As I mentioned above, XPM is available in the contrib sections of R4, R5, and R6. If you don't already have the R6 contrib directory, I suggest picking up XPM from the R6 distribution -- the latest version is XPM-3.4c and should be available from your favorite X ftp site (ftp.x.org and ftp.crl.research.digital.com are but two in the Northern United States). While you're there, I recommend picking up pixmap-2.6 as well. Pixmap 2.6 is an editor for creating and editing XPM files. It is fairly easy to use and should be very familiar to anyone who has used the R5 or R6 "bitmap" program -- in fact, it is based on that same interface, with some enhancements to deal with colors, etc.

Both XPM-3.4c and pixmap-2.6 come with Imakefiles, so building them should be a simple task to the Imake-initiated. If you're new to this game, jump right in by reading the README files in each of those distributions, and they'll get you started. Fortunately, both packages are relatively easy to build. I built them both with little tweaking involved.

Got It? Good, Let's Begin...

Once you have Pixmap built, play with it for a little while and save out a file or two. Now let's take a look at the file format. Since examples make good teachers, here is a sample, followed by some explanation:

/* XPM */
static char *happy_face[] = {
/* smiley
* width height ncolors chars_per_pixel hotspot_x hotspot_y */ "25 15 4 1 13 8",
/* colors */
": c None	m None g clear s clear",
"X c green m black g black s outline",
" c yellow m white g gray85 s fill_color", "o c red	m black g gray10 s eye_color",
"B c blue	m black g gray30 s smile_color",
/* pixels */
"::::::::::XXXXX::::::::::",
":::::XXXXX	XXXXX:::::",
"::XXX	XXX::",
":X			X:",
"X	oo	oo	X",
"X	oo	oo	X",
"X	oo	oo	X",
"X	X",
"X BBB	BBB  X",
"X B	B 		X",
"X BB	   		BB	X",
":X	BBBBBBBBB	X:",
"::XXX	XXX::",
":::::XXXXX	XXXXX:::::",
"::::::::::XXXXX::::::::::"
};

Now, to explain. Each XPM file is made up of six parts. The initial line is the Header and consists of a comment "/* XPM */". The second line is an identifier and specifies the name of the file, and also the name of the string array variable, should the file be included in C code -- in this case, "happy_face".

The next section is the Values section, which gives either four or six integers describing the pixmap width, height, number of colors used, number of characters used to represent each color in the character array, and optionally the x and y hotspot coordinates. The hotspot is useful in the creation of cursors and when defining Pixmaps that may be used in animation. An additional tag XPMEXT may be specified if there is any extension defined -- I will not go into extensions here, however. The example above defines a 25-by-15 pixmap, containing four colors, each represented by a single character, and a hotspot right in the middle at 13, 8.

The following section describes the Colors being used. Here we have one line for each of the ncolors we specified in the previous section. Each of the Colors lines consists of the characters used to represent the colors in the character array of the next section, followed by a number of key-color pairs. The key may have one of the following values, and specifies the color to be used when the final Pixmap is displayed on the corresponding visual type:

m	monochrome visual
g4	four-level greyscale visual
g	grayscale visual with more than four levels
c	color visual
s	symbolic name useful for end-user customization

In the example above, there is a symbolic color named "smile_color" which will appear blue on a color display, black on a monochrome display, and gray30 on a grayscale display. The colors may be specified by name or via an RGB triplet preceded by the pound-sign (#). In the future, one will be able to specify HSV triplets preceded by percent (%), though this is not yet implemented. Note that the example does not include the "g4" key -- if an exact match for the visual type is not found among the choices, the next closest visual's key-color pair is used instead.

Lastly, we come to the representation of the pixmap itself. By this point it should become obvious how the resulting Pixmap will appear -- on a color display, we will see a yellow smiley with red eyes and a blue smile, surrounded by a green border, while on a grayscale monitor, we will see varying shades of gray, and on a monochrome screen only the eyes, mouth and border appear black on a white background.

Note the use of the word "None" as a color. This is a special keyword which indicates a "transparent" pixel. Now, "transparent" pixels do not really exist in X, but what this allows is the creation of a mask by XPM. Any pixel which is not transparent becomes part of the mask pixmap, which can then be used as a clipping mask in an X GC (Graphics Context) or as a shape mask for a window if using the X Shape Extension.


The care and feeding of XPM Files

XPMUs are storable as Ascii text in plain files, which would imply that there are functions for reading and writing them to and from files. Also, perhaps slightly less obvious, are functions for creating XPMUs from strings and converting them to strings. Their format also makes them suitable for direct inclusion into C source code.

With these functions in mind, refer to Table 1. This table summarizes the majority of the API calls that most programmers will be interested in. There are a number of other functions available, but they are mostly useful for programs that manipulate the data internal to the XPM, such as Pixmap editors, and so I will not go into those here.

Table 1: XPM functions
>From	To: XPM File	XImage	Pixmap
============================================================================== 
XPM File | N/A	WriteFileFromImage	WriteFileFromPixmap
XImage	| ReadFileToImage N/A
Pixmap	| ReadFileToPixmap	N/A
XPM Data | ReadFileToData CreateDataFromImage CreateDataFromPixmap XPM Buffer | ReadFileToBuffer CreateBufferFromImage CreateBufferFromPixmap 


Table 1: XPM functions (cont'd)
>From	To: XPM Data	XPM Buffer
=========================================================== XPM File | WriteFileFromData	WriteFileFromBuffer
XImage	| CreateImageFromData CreateImageFromBuffer
Pixmap	| CreatePixmapFromData CreatePixmapFromBuffer
XPM Data | N/A
XPM Buffer |	N/A

To conserve space, I've left off the initial RXpmS in each of the function names. Thus, RWriteFileFromPixmapS is actually RXpmWriteFileFromPixmapS. Other notes on reading the table:

Because Pixmaps are so commonly used, of the function calls above, most people will only need use XpmReadFileToPixmap and XpmCreatePixmapFromData, so I will go into them in some detail below.

XpmCreatePixmapFromData

For performance reasons, you may choose to include your XPM files directly into your programs. This obviates the need to read the file in from disk at run-time, perhaps at the expense of loss of configurability. If there is a particular image you wish to ensure is defined, however, this may be the right choice for you. Having done so, you will then have a string array available to you which you can then convert to a Pixmap for further use in Xlib, Xt, or Motif calls.


int XpmCreatePixmapFromData(display, d, filename, pixmap_ret, shapemask_ret, attrs); 
	Display *display;
	Drawable d;
	char **data;
	Pixmap *pixmap_ret;
	Pixmap *shapemask_ret;
	XpmAttributes attrs;

The Display pointer and Drawable parameters should be well-known to X programmers - they specify the connection to the X server and the window that the Pixmap should be created for. (Technically, a Drawable may be something besides a window, but that is a topic for another day.) The character array is the variable defined by the XPM file that you included.

The next two parameters are pointers to returned data - the Pixmap defined by the character array and the shapemask for the image. The shapemask may be useful for specifying the clipping mask for a GC (Graphics Context), or for creating non-rectangular windows with the X Shape Extension.

The final parameter is a pointer to a XpmAttributes structure which is returned to the caller. This structure is explained in more detail below.

XpmReadFileToPixmap

If you choose instead to read files on the fly, use XpmReadFileToPixmap. Reading Pixmaps from files allows for greater flexibility - if you ship your application with a set of XPM files, the end-user can replace them as he wishes to customize the look of your application. Or perhaps the images you will need to use in your application will not be determined until run-time.


int XpmReadFileToPixmap(display, d, filename, pixmap_ret, shapemask_ret, attrs); 
	Display *display;
	Drawable d;
	char *filename;
	Pixmap *pixmap_ret;
	Pixmap *shapemask_ret;
	XpmAttributes attrs;

As with XpmCreatePixmapFromData, the Display pointer, Drawable, Pixmap pointers and XpmAttributes pointer are the same; the only parameter which changes is a filename instead of a string array.

XpmAttributes

The XpmAttributes structure (used in both of the calls above) is defined as follows:


typedef struct {
	unsigned long valuemask;	/* which attributes are defined */
	Visual *visual;			/* the visual to use */
	Colormap colormap;		/* colormap to use */
	unsigned int depth;		/* depth of the pixmap */
	unsigned int width;		/* the width of the pixmap */
	unsigned int height;		/* the height of the pixmap */
	unsigned int x_hotspot;	/* hotspotUs x coordinate */
	unsigned int y_hotspot;	/* hotspotUs y coordinate */
	unsigned int cpp;			/* characters per pixel */
	Pixel *pixels;			/* list of used color pixels */
	unsigned int npixels;		/* number of pixels */
	XpmColorSymbol *colorsymbols; /* array of color symbols to override */
	unsigned int numsymbols;	/* number of symbols */
	char *rgb_fname;		/* rgb text file name */
	unsigned int nextensions;	/* number of extensions */
	XpmExtension *extensions;	/* array of extensions */
	int ncolors;			/* number of colors */
	XpmColor *colorTable;		/* color table pointer */
	char *hints_cmt;		/* comments in hints section */
	char *colors_cmt;		/* comments in colors section */
	char *pixels_cmt;		/* comments in pixels section */
	unsigned int mask_pixel;	/* transparent pixels color table index */
					/* color allocation directives */
	unsigned int exactColors;	/* only use exact colors for visual */
	unsigned int closeness;	/* allowable RGB deviation */
	unsigned int red_closeness;	/* allowable red deviation */
	unsigned int green_closeness; /* allowable green deviation */
	unsigned int blue_closeness; /* allowable blue deviation */ 
	int color_key;			/* use colors from this color set */
} XpmAttributes;

Most of the structure is self-explanatory. The valuemask is used primarily with those API functions where the programmer will be changing some part of the Pixmap, or if the programmer wishes to ensure that certain information is returned. For example, if you wish to know the width and height of the Pixmap created using XpmReadFileToPixmap, you would have set the XpmSize flag in the valuemask. A complete list of flags can be found in the file Rxpm.hS.

Other items of interest in the XpmAttributes structure are the exactColors and closeness members - they allow the programmer to specify whether the colors must be allocated exactly, or if not, how close is acceptable. Many times, if your current colormap is near full, "close enough" is good enough. On rare occasions you may require a pixmap to look exactly as it was designed, but this is not usually the case. Also note the colorsymbols element - as I mentioned last month, the XPM format allows the end-user to override the colors stored in the XPM via the symbolic name stored in the XPM.

You might well not even care about the information in this structure, or be interested in overriding the colors or specifying their exactness. In this case you may pass in a NULL pointer to XpmReadFileToPixmap or XpmCreatePixmapFromData - however, if you do pass in a valid pointer, be sure that the valuemask is set to at least zero, else unpredictable results may occur.

For more information

While the information here should be enough to get you started, if you need more detail, I recommend reading the documentation file in the "doc" directory of the xpm-3.4c distribution. Additionally, there is a mailing list for discussion of XPM called "xpm-talk@sophia.inria.fr". Subscription requests should be sent to "xpm-talk-request@sophia.inria.fr".


Implementing a message catalog in Xrm

The last column covered some of the basic issues involved in I18N (internationalization). One of the topics I brought up involved the localization of applications through the use of app-defaults files. Today I'll go a little more in-depth on how one might implement a message catalog in Xrm, as well as the pros and cons of such a system versus another message catalog standard, the XPG3 routines catopen(), catgets(), and friends.

First a little review. Typically, app-defaults are used to store widget resources such as fonts, colors, cursors, and so on. The major widget sets based on Xt (Motif, Open Look and Athena widgets) all provide these resources to the developer and end-user. End-users appreciate the ability to customize applications to their liking, but perhaps more important to the developer is the ability to localize an application and thereby sell into multiple (international) markets.

While it is a laudable idea to extract as many of these resources into an app-defaults file in the name of ease of localization, the problem is that these resources specify only the static representation of the interface. By "static", I mean that it is only possible to supply one value. Suppose, however, that I have a text label which is used to indicate some status of my application. At varying times, the value of the text label will display different values. The only way to change the value is programmatically via a call to XtSetValues(). Given that these functions are hard-coded into the application, the natural inclination is to hard-code the resource values into the application as well. Doing so, however, would obviously defeat the ease of localization that we have been striving for. Thus, what is needed is some method to look up the various values for the text label at run-time and then call XtSetValues() with the value just retrieved from that message catalog.

The same logic can be applied to resources other than text labels. As I mentioned last month, use of color and their meanings varies widely from one culture to another. Therefore, if instead of (or even in addition to) the varying text label hypothesized above, I have a button whose background color changes in response to some internal application state, it seems a good idea to give the colors some symbolic names (for example, "warningColor") and then look them up at run-time.

The code

So, without further adieu, let's get down to some coding. Listing 1 is a short program which demonstrates most of the capabilities that we are looking for, implemented in Xrm (the X Resource Manager). The first thing that the program does is initialize Xrm. This must be performed before any other Xrm calls, or unpredictable results, will occur. XrmInitialize installs a single resource converter for us, providing string-to-string conversion. This is the degenerate case of converters, of course; the converter does basically nothing. It is important to remember this, however, since this means that we will only be able to retrieve string values from our resource database. Xt-based programs will be able to take advantage of string-to-color, string-to-cursor, string-to-boolean, and other converters. The exact converters available will depend on the widget set employed, of course, but some basic ones can be assumed.

Returning to our program listing, the next line opens a file database and returns a pointer to that database. It is also possible to get the database associated with a display, but for the purposes of a simple message catalog, let's continue with the use of a file. After successfully retrieving the database, we can now begin to get resources out of it, using XrmGetResource(). This function takes a number of arguments and deserves some explanation.

The first argument is the resource database we opened earlier. The second and third arguments are the instance and class of the resource we wish to retrieve. In the example, I have used "error1" for the instance, and "Error" for the class. If it's unable to locate the specific instance I request, it will return the value for the class instead. In this way I can provide a fallback value -- for example, if all of my error messages are named "error1", "error2", "error3", and so on, and I specify the class "Error" for each when getting them, I can get a generic error message if Xrm is unable to locate one of the specific errors.

Listing 2 provides the test database which accompanies the program in Listing 1. Changing "error1" to "error2" in my program listing would cause the program to output "An error has occurred." instead of "Error number 1 has occurred. Call your local authorities." If several error conditions can be served by a common message, they can all retrieve the class message by specifying a bogus instance which will not be found. I can also use this behavior to my advantage when it comes time to localize the application. If translating to Spanish, I can translate the "Error" class string first, then add translations for each of the specific instances ("error1", "error2", etc.) as time permits.

Getting back to our listing once again, the "str_type" and "xrmval" variables deserve explanation. "str_type" will contain a string describing the type of resource returned. This will vary depending on what string converters are available, but in our case it will always contain the value "String". When other converters are used, however, this value becomes important, as it determines how the program should handle the "xrmval" return value. If "str_type" were to contain "Boolean", we would know that the value contained in "xrmval" was a boolean and should be treated as such. The XrmValue struct contains two elements, "size" and "addr". "size" obviously gives the size of the value pointed to by "addr", and "addr" is an opaque type (an XtPointer) which can point to any type of data. When referencing xrmval.addr, you will likely want to cast it to a type depending on "str_type" as described above.

Listing 1


#include 
#include 
#include 

main(int argc, char **argv)
{
XrmDatabase xrmdb;
Bool ret;
XrmValue xrmval;
char *str_type[20];

XrmInitialize();
xrmdb = XrmGetFileDatabase("test.db"); /* returns an Xrm DB */ if (xrmdb == NULL)
{
fprintf(stderr, "unable to open test.db\n"); exit(-1);
}
ret = XrmGetResource(xrmdb,	/* DB from above */
"error1",	/* name of resource */
"Error",	/* class of resource */
str_type,	/* return representation type */
&xrmval);	/* resource value, do not modify or free! */

if (ret == True)
printf("%s\n", (char *) xrmval.addr);
else
fprintf(stderr, "unable to find error string\n"); 

exit(0);
}

Listing 2

*Error:	An error has occurred.
*error1:	Error number 1 has occurred. Call your local authorities.

Observations

Reviewing the listing, you may have noticed that it does not make use of Xt, nor does it ever open a connection to the X server. Most applications will be using Xt and an X display, of course, and I will address those issues a bit further along in the article. But, the fact that neither were used in this first example brings up an interesting point: the X resource mechanism can be used by programs which have no X interface whatsoever -- it is, in fact, possible to implement a general-purpose message catalog using Xrm.

There are other things worth noting -- for example, suppose I have two versions of an application, one GUI-based, the other CLI (command line interface) driven, for use on ASCII terminals. Users could supply some specific preference in both applications in their .Xresources files, and both versions of the program could reference that preference, thereby making both versions behave the same under the same circumstances.

By way of illustration, I might have a GUI-based "trashcan" program which allowed one to delete files easily using the mouse. Being a destructive operation, I wish to allow the user to specify whether he should be prompted before the deletion occurs. I might define a resource called "promptOnDelete" to allow him to turn this behavior on and off. The CLI version, "rm", could also be modified to read this resource and act like "rm -i" when true.

If writing an Xt-based application, calling XrmInitialize() is unnecessary, as it will be performed automatically when XtAppInitialize() is called. The current database associated with the application on that display may be retrieved with XtDatabase() instead of XrmGetFileDatabase(), but whatever method you choose to get a database, you may then proceed to retrieve values from it using XrmGetResource() as above.

Under Xt, however, it probably is better (and easier) to use XtGetApplicationResources() (or the varargs version, XtVaGetApplicationResources()) to retrieve a list of resources. By setting up an XtResourceList and a structure to hold values in, it is possible to retrieve a large number of values at one time, as well as specify the type of conversion to be performed and a default value should the resource not exist.

Pros and Cons

There are several advantages to implementing a catalog as described above:

There are a number of disadvantages as well, however:

One could probably come up with many other pros and cons, but as with any development project it is a matter of tradeoffs, choosing which method best meets your needs. Whether the use of Xrm as a cataloging tool works in your situation, only you can decide. But I hope I have at least shown that Xrm is a viable alternative as a general-purpose catalog, and have provided something to think about, whether you choose to use it or not.


A first look at CDE

For many years, Microsoft Windows and Macintosh users have become comfortable with working in a "desktop" environment. They are able to manipulate files and folders using a drag-and-drop metaphor, launch applications by double-clicking on them, and experiment with and select common attributes of applications such as font and colors via some sort of preferences manager. This ease of use has certainly contributed to the widespread acceptance of these two environments.

While similar environments exist in the workstation world (most notably, Sun's OpenLook, SGI's Magic and HP's VUE), the biggest hurdle to each of their successes was the fact that each was proprietary and operated in different ways. This may be fine in homogeneous workplaces, but it is not atypical to encounter several kinds of workstations from different vendors in the average workplace. Thus, the average user is faced with having to learn multiple methods of performing the same task, each with its own set of quirks.

Enter CDE, the Common Desktop Environment. CDE was designed to break down these differences and provide the user with a common operating environment, regardless of the workstation vendor or software provider. CDE was developed jointly by SunSoft, HP, IBM and Novell to be an open system, freely licenseable by any vendor who chooses to implement it. In much the same way that the X Window System has become the industry de-facto windowing system replacing each vendor's proprietary window system, so CDE is hoped to become the de-facto desktop environment in the X world.

Besides making the user's life easier by providing a common desktop, CDE will make developers' lives easier as well. Developers need only to write to one API to ensure portability across many hardware vendors. So too, the system administrator's life is made easier -- common installation procedures make adding new software a snap. That said, let's concentrate on the features that make CDE attractive and interesting from a user's standpoint.

The components of CDE

One of the goals of CDE was to use existing technology wherever possible, thus not only reducing the time to market, but also speeding the standards acceptance process (the design is being driven in large part by the X Consortium, the OSF and X/Open). The existing technologies chosen that will be most familiar to end users are Motif and HP's VUE desktop environment.

Motif has become the de-facto standard for X toolkits, so it is an obvious choice to build upon. The Motif 1.2 version became the base for the CDE version of Motif. Several widgets were added to Motif 1.2 to help smooth the transition from other window systems, as well as to provide functionality needed elsewhere on the desktop. Motif 2.0 was not available at the time that the CDE work began, but much of the work on CDE has since been folded into Motif 2.0, and it seems likely that the gaps between CDE and Motif 2.0 will narrow over time.

Motif is simply a toolkit, though -- what of the real applications that make up the desktop? As mentioned above, current users of HP's VUE will notice little difference between it and CDE.

The first obvious encounter with CDE is at the login screen. This is implemented as a Motif front end to X11R5's xdm (X Display Manager). From the login screen, the user may select from two session snapshots, the "home" session (a configurable arrangement of windows and applications that is the same each time), or the "current" session (the way things were left at the previous logout). This is made possible via the session management capabilities of CDE.

When logging out, the session manager instructs each running application to shut itself down and directs it to a file in which each should save its state, including which files are currently being operated on, or any other application-specific information needed to recreate that state upon restart of the application. Similarly, the session manager snapshots the window positions and notes which applications are running, so that upon logging in, it can restart those applications, and point them at their individual saved state files.

The second most obvious component of CDE is known as the Front Panel. The Front Panel is a long rectangular box at the bottom of the desktop that typically contains a clock and the date as well as a number of buttons for launching common applications and controlling the workspace.

The Workspace Manager is manipulated via a number of buttons in the center of the Front Panel. Each workspace can contain a number of distinct windows and applications, so the workspace concept is similar to the functionality provided by virtual window managers, or perhaps more accurately, the "rooms" functionality provided in other window managers. Clicking on a button warps the desktop to the corresponding workspace and the windows it contains. (As a point of information, it is interesting to note that the Workspace Manager, the Front Panel, and the CDE Window Manager "dtwm" are all actually a single process. "dtwm" is based on "mwm" and thus will be familiar to users of that window manager.)

Other items on the Front Panel include a screen-lock button and a logout (or "exit") button. Those two are hopefully self-explanatory; there are also buttons for the Style Manager, the File Manager, and the Help Manager, each of which deserve further mention.

The Style Manager provides an easy method for users to customize the desktop and some common application attributes via a GUI. While customization may also be performed via the X Resource Mechanism, most users are intimidated by X Resources and only really want to set a few attributes such as mouse acceleration, double-click speed, desktop background pattern, standard fonts and color schemes. Color schemes group a set of foreground, background, highlight, shadow and other colors into a descriptively named bunch, thus the user can choose a scheme rather than specify each of the individual colors.

The File Manager is a familiar concept to most users -- files may be dragged and dropped into folders, applications are launched by double-clicking on them, etc. Probably the most interesting feature is the ability to not only drag and drop between windows in the file manager, but also to and from applications. For example, to attach a file to a MIME mail message, one simply drags it from the File Manager to the mail utility. Likewise, MIME attachments may be dragged from the mail utility to the desktop, or even simply double-clicked while still attached to the mail message to view them (for example, a GIF file might launch "xloadimage" as a viewer), or even run directly (if the attachment is a shell script, for example).

While having drag-and-drop "aware" applications may be a new concept to many users, it has of course existed for some time in the OpenLook desktop environment. However, with the standardization on CDE, multiple vendors will all be using the same mechanism to communicate among applications, thus greatly extending the usefulness of that functionality.

Last, but certainly not least, is the Help Manager functionality. At last, there is a common look and feel and mechanism for providing application support. Two new widgets (alluded to above) were implemented to support the Help system. The simpler of the two, the "Quick Help dialog," is useful for definitions, glossary items or other small bits of self-contained information. The larger and more interesting "Help dialog" provides a complete hierarchical browser. A table of contents may appear at the top, as well as navigational controls to move forward and back.

Also available are rich text support (fully I18N compliant, of course), and hyperlinks within text. Users of popular WorldWide Web (WWW) browsers like Mosaic and Netscape should feel quite at home in the CDE Help system. One interesting new twist, however, is the ability to have links not only to other help pages, but links that launch an external application when clicked on. This functionality opens up new possibilities in the way help can be provided. For example, when faced with a tricky problem, a user might consult the Help system, where the developer may have included several possible actions to pursue to resolve the problem. Then the user could activate one of the suggestions simply by clicking on the text describing it.

It is impossible to cover all of the functionality of CDE in a space such as this. Each of the topics above could easily fill a chapter of a book (and probably will in the forthcoming documentation set). But, perhaps this brief overview has given you an insight into the future. And maybe given you hope that someday X users will even catch up to those PC users.


Toolkit tips and tricks

There are a number of tips and tricks that you can use to enhance the functionality of your Xt-based programs. The techniques presented here are perhaps among the most often-asked about in newsgroups and mailing lists, so I'll try to explain them in some detail here. The specific questions I hope to address are: 1) How can I perform idle-time processing? 2) How can I deal with other forms of input (besides X events)? 3) How should I handle signals?

Idle-time processing

Xt provides the ability to add "work procedures" that are automatically called when there are no X events waiting to be processed. In general, however, one should make sure that a work procedure not take very long to complete, or else the user will notice a delay in handling button clicks, window redrawing, and the like. Thus, it is probably not a good idea to use a work procedure to compute PI to the millionth decimal place -- at least, not all at once.

What I mean by that last comment is that it is possible to perform complex tasks in a work procedure IF the task can be easily broken down into small chunks. Simply keep track of the state of the task in a variable (or variables) static to the work proc, or even globally if that makes more sense for you. If the task is one of simple iteration, you can perform one or two iterations each time the work procedure is called, based on your saved static variable. If the task requires a series of distinct steps to be performed, the work proc could be written as a case statement, with the static variable holding the state of your finite state machine. Or, perhaps even a combination of these techniques is appropriate.

Having said all that, I suppose I should tell you how to create and add a work proc to your application. The function for adding a work proc is as follows:

XtWorkProcId XtAppAddWorkProc(XtAppContext app_context,
XtWorkProc proc,
XtPointer client_data);

It requires an application context (which you received from XtAppInitialize, initializing the toolkit), the procedure to be called and some piece of data to be passed to the procedure. The client_data may be anything you wish -- XtPointer is an opaque type that may point to anything, and it is up to the developer to ensure that the work procedure will have been written to know how to deal with the data passed to it. Of course, you need not pass anything, if you wish. Simply use NULL instead, and ignore the client_data in the work procedure.

An XtWorkProc, then, obviously takes one argument -- the client_data from above -- and is typedef'ed as follows:
typedef Boolean (*XtWorkProc)(XtPointer client_data);

The function should return a Boolean value -- either True, indicating that idle processing is complete and the work proc can be removed, or False, in which case the procedure will remain registered and called again the next time there are no X events waiting to be processed. Multiple workprocs may be registered, but only the most recently added one is called. Removing the current one effectively "pops the stack" so that the next most recently added one is called instead. To manually remove a work procedure, call XtRemoveWorkProc, passing it the XtWorkProcId that was returned to you when the procedure was added. If you no longer need to do idle-time processing, it is a good idea to remove your work proc so that the toolkit need not waste the time calling it.

Other forms of input

Besides the usual buttons clicked and keys pressed, it is conceivable that you may wish your application to react to input from a pipe, stream or socket. Fortunately, the X toolkit provides a method to deal with such circumstances by allowing you to call an arbitrary procedure based on data waiting to be read from or written to a file descriptor. These alternate forms of input are checked for and handled in the same way that X events are handled; it is not the same as work procedure, where preference is given to X events.

To install an input procedure, call XtAppAddInput as follows:

XtInputId XtAppAddInput(XtAppContext app_context,
int source,
XtPointer condition,
XtInputCallbackProc proc,
XtPointer client_data);

The app_context, once again, is the application context that was created for you when the toolkit was initialized, and client_data is any data that you might wish to pass to the input procedure "proc". The "source" is the file descriptor that you wish to watch for, and "condition" is one or more of XtInputReadMask, XtInputWriteMask, or XtInputExceptMask.

The use of XtInputReadMask is probably most common, since most likely you will want to be notified when there is data waiting to be read from a file descriptor. The use of XtInputWriteMask is used when you wish to be notified a descriptor is ready for writing -- perhaps the use of the word "input" everywhere is a poor choice because it can be used to handle output as well. And lastly, XtInputExceptMask is used for exception conditions (for example, a broken pipe or closed socket). These masks may be combined if you wish to use one procedure to handle multiple cases, though this is probably not the usual case.

Here is the typedef for an input procedure:

typedef void (*XtInputCallbackProc)(XtPointer client_data,
int *source,
XtInputId *id)

The client_data passed in is that which was given when the procedure was registered, the source is the file descriptor on which there is activity, and the id is what was returned to you when you registered the callback.

Signal processing

It is often tempting to put a substantial amount of code into a signal handler to deal with the signal just received; however, this temptation must be resisted. According to the ANSI C standard, calling anything besides signal() to reset the handler from within a signal handler is not guaranteed to work. The results are unpredictable at best, and at worst can cause a core dump. This is especially true with Xlib, Xt and other toolkit (Motif, Xview, etc.) calls, because the application may be in the middle of sending an X request to the X server, and calling another X function may then begin sending a different request -- to the server it will look like garbage. The internal state of any of the libraries may also become corrupt, as many calls are not protected against being invoked multiple times simultaneously.

Perhaps the best way to approach this is to set a global flag in the signal handler that can then be picked up and dealt with later by a work procedure (as described above). Note that it is not safe to install a new work procedure in the course of the signal handler; the work proc should have been installed in the normal flow of the application.

Some people have pointed out that this method has the drawback that the signal does not get processed in a timely fashion -- the control of the application has to pass back to the XtAppMainLoop before the work procedure will be called. This can potentially be a significant delay, if the signal is delivered in the course of a lengthy callback. However, this is an unavoidable problem.

Another drawback to this method is that all of your work procedures must be written to deal with the flag(s) set by the signal handler(s). Otherwise, if a work proc that does not handle the flag(s) is temporarily installed, the action you wish to perform on receipt of a signal will never occur until that temporary work proc is removed. One way around this is as follows:

Instead of using a flag to keep track of signals received, open a pipe() in the main flow of the application. Attach the "read" end of the pipe to an input handler as described above. In the input procedure, you then take whatever action you desire to deal with the signal. In the actual signal handler, write a byte to the "write" end of the pipe. When control returns to the XtAppMainLoop, the input proc will be called. In this way, it is not necessary to deal with signals in your work procedures, nor need you even install one. This also has the added benefit that the input proc will only be called when there is data waiting to be read on the pipe, instead of being called whenever the application is "idle," as is the case with the work proc.